/* xslt.c
 * XSLT transformations for ConArtist
 */

/*
 * Licensed to the ConArtist Development Team (CADT).
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 * The CADT licenses this file to you under the Apache License, 
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* TODO 
 * - add an error handling routine to pipe messages into the ConArtist
 *   log
 * - figure out how to handle in-memory stylesheets wrt finding includes
 */

#include "config.h"

#include <apr_strings.h>

#include <libxml/tree.h>
#include <libxslt/xslt.h>

#include <libxml/globals.h>
#include <libxml/threads.h>
#include <libxml/xinclude.h>
#include <libxml/xmlIO.h>
#include <libxslt/xsltutils.h>
#include <libxslt/transform.h>
#include <libexslt/exslt.h>
#include <libxslt/security.h>
#include <libxslt/documents.h>
#include <libxslt/xsltInternals.h>

#include "conartist.h"
#include "caLog.h"
#include "caPipeline.h"
#include "caGenerator.h"

static xmlExternalEntityLoader defaultEntityLoader = NULL;
static apr_pool_t *xsltPool = NULL;

struct xsltData {
    apr_pool_t       *pool;
    char             *templateFn;
    char             *inputFn;
    char             *fsPath;
    xsltStylesheetPtr transform;
    apr_hash_t       *paths;
    int               options;      /* XSLT options */ 
};
static apr_hash_t *pathHash = NULL;

static xmlDocPtr
xsltDocLoaderFunction(const xmlChar * URI, xmlDictPtr dict, int options, 
                      void * ctxt, xsltLoadType type)
{
    xmlDocPtr ret = NULL;
    apr_hash_index_t *hi = NULL;
    const char *lastsegment = (const char *)URI;
    int i = 0;

    while (URI[i] != '\0') {
        if (URI[i] == '/')
            lastsegment = (char *)&URI[i + 1];
        i++;
    }

    for (hi = apr_hash_first(xsltPool, pathHash); hi; 
         hi = apr_hash_next(hi)) {
        xmlChar *path = NULL;
        xmlChar *newURL = NULL;
        void *pathPtr;

        apr_hash_this(hi, NULL, NULL, &pathPtr);
        path = pathPtr;

        newURL = xmlStrdup(path);
        newURL = xmlStrcat(newURL, (const xmlChar *)"/");
        newURL = xmlStrcat(newURL, (const xmlChar *)lastsegment);
        ret = xmlParseFile((char *)newURL);
        if (ret)
            caLog(CA_LOG_INFO, "Resolved '%s' into file path '%s'",
                  (char *)URI, (char *)newURL);
        xmlFree(newURL);
        if (ret)
            break;
    }
    if (!ret)
        caLog(CA_LOG_WARN, "Unable to resolve '%s' into a file path",
              (char *)URI);

    return ret;
}


static xmlParserInputPtr
xsltExternalEntityLoader(const char *URL, const char *ID,
			 xmlParserCtxtPtr ctxt) 
{
    xmlParserInputPtr ret;
    apr_hash_index_t *hi = NULL;
    const char *lastsegment = URL;
    int i = 0;
    
    if (defaultEntityLoader) {
        ret = defaultEntityLoader(URL, ID, ctxt);
        if (ret)
            return ret;
    }

    while (URL[i] != '\0') {
        if (URL[i] == '/' || URL[i] == ':')
            lastsegment = &URL[i + 1];
        i++;
    }

    for (hi = apr_hash_first(xsltPool, pathHash); hi; 
         hi = apr_hash_next(hi)) {
        char *path = NULL;
        xmlChar *newURL = NULL;
        void *pathPtr = NULL;

        apr_hash_this(hi, NULL, NULL, &pathPtr);
        path = pathPtr;
        newURL = xmlStrdup((const xmlChar *)path);
        if (lastsegment[0] != '/')
            newURL = xmlStrcat(newURL, (const xmlChar *)"/");
        newURL = xmlStrcat(newURL, (const xmlChar *)lastsegment);
        ret = defaultEntityLoader((char *)newURL, ID, ctxt);
        if (ret)
            caLog(CA_LOG_INFO, "Resolved '%s' into file path '%s'",
                  (char *)URL, (char *)newURL);
        xmlFree(newURL);
        if (ret)
            break;
    }

    if (!ret)
        caLog(CA_LOG_WARN, "Unable to resolve '%s' into a file path",
              (char *)URL);

    return ret;
}

static apr_status_t xsltProcess(struct caPipeData *pd, struct caPipe *p)
{
    struct xsltData *xd = (struct xsltData *)p->privateData;
    apr_status_t rv = APR_EINVAL;
    xmlDocPtr doc = NULL;
    xmlDocPtr result = NULL;
    xmlChar *tmpMem;
    int memSz = 0;
    xsltStylesheetPtr transform = NULL;
    xsltTransformContextPtr xsltCtx = NULL;
    int options = XSLT_PARSE_OPTIONS;

    if (!xd->inputFn && (!pd->input || pd->iLen == 0))
        return APR_EINVAL;

    /* Parse input data */
    if (xd->inputFn) {
        caLog(CA_LOG_DEBUG, "XSLT: reading input file '%s'", xd->inputFn);
        doc = xmlReadFile(xd->inputFn, NULL, 0);
    } else
        doc = xmlParseMemory(pd->input, pd->iLen);

    if (!doc) {
        caLog(CA_LOG_WARN, "XSLT: unable to process input document %s",
              (xd->inputFn ? xd->inputFn : "from input"));
        return APR_EINVAL;
    }

#if LIBXML_VERSION >= 20603
    xmlXIncludeProcessFlags(doc, XSLT_PARSE_OPTIONS);
#else
    xmlXIncludeProcess(doc);
#endif

    if (!xd->transform) {
        if (strcmp((char *)xd->templateFn, "OUTPUT") == 0) {
            /* xslDoc will be freed with the call to xsltFreeStylesheet */
            xmlDocPtr xslDoc = xmlParseMemory(pd->output, pd->oLen);
            xsltSetLoaderFunc(xsltDocLoaderFunction);
            transform = xsltParseStylesheetDoc(xslDoc);
            xsltSetLoaderFunc(NULL); /* restore default */
        } else {
            xd->transform = xsltParseStylesheetFile((xmlChar *)xd->fsPath);
            transform = xd->transform;
        }
    } else
        transform = xd->transform;

    if (!transform) {
        caLog(CA_LOG_ERROR, "Unable to parse stylsheet from '%s'",
              xd->templateFn);
        goto clean_exit;
    }

    xsltCtx = xsltNewTransformContext(transform, doc);
    if (!xsltCtx) {
        caLog(CA_LOG_ERROR, "Unable to create an XSLT context");
        goto clean_exit;
    }
    xsltCtx->_private = xd;

    xsltSetCtxtParseOptions(xsltCtx, options);

    result = xsltApplyStylesheetUser(transform, doc, NULL, NULL, NULL, 
                                     xsltCtx);
    if (!result) {
        caLog(CA_LOG_WARN, "No output returned from XSLT processing using "
              "template '%s'", (char *)xd->templateFn);
        goto clean_exit;
    }

    xmlDocDumpMemory(result, &tmpMem, &memSz);
    if (memSz > 0) {

        pd->output = apr_palloc(pd->pool, memSz);
        if (pd->output) {
            pd->oLen = memSz;
            memcpy(pd->output, tmpMem, memSz);
            rv = APR_SUCCESS;
        }
    }

    if (rv == APR_SUCCESS)
        caLog(CA_LOG_INFO, "XSLT transformation of '%s' completed OK, %d bytes",
              xd->templateFn, pd->oLen);

clean_exit:
    if (transform != xd->transform)
        xsltFreeStylesheet(transform);
    if (doc)
        xmlFreeDoc(doc);
    if (result)
        xmlFreeDoc(result);
    return rv;
}

apr_status_t xsltPipePrepare(struct caPipe **pp, struct pipelineInstance *pi,
                             xmlNode *node)
{
    struct caPipe *p = NULL;
    xmlNode *n;
    struct xsltData *xd = NULL;
    apr_status_t rv = APR_SUCCESS;

    if (!PI_POOL(pi))
        return APR_ENOPOOL;

    p = apr_pcalloc(PI_POOL(pi), sizeof(*p));
    xd = apr_pcalloc(PI_POOL(pi), sizeof(*xd));
    if (!p || !xd)
        return APR_ENOMEM;

    xd->options = XSLT_PARSE_OPTIONS | XML_PARSE_NOENT | XML_PARSE_XINCLUDE;
    xd->paths = apr_hash_make(PI_POOL(pi));
    xd->pool = PI_POOL(pi);

    p->func = (pipeProcessFn *)&xsltProcess;
    p->rValue = APR_SUCCESS;

    for (n = node->children; n; n = n->next) {
        xmlChar *data = xmlNodeGetContent(n);
        if (xmlStrcmp(n->name, (xmlChar *)"Template") == 0)          
            xd->templateFn = apr_pstrdup(PI_POOL(pi), (char *)data);
        else if (xmlStrcmp(n->name, BAD_CAST "Input") == 0) {
            rv = caGeneratorGetFilename(&xd->inputFn, BAD_CAST data, pi->gen);
        } else if (xmlStrcmp(n->name, (xmlChar *)"TemplatePath") == 0) {
            char *thepath;
            rv = caGeneratorGetFilename(&thepath, (char *)data, pi->gen);
            if (rv == APR_SUCCESS) {
                apr_hash_set(xd->paths, thepath, APR_HASH_KEY_STRING, thepath);
                caLog(CA_LOG_INFO, "Added template path '%s' as '%s' to XSLT pipe",
                      (char *)data, thepath);
                /* This is a hack to get round the problems with
                 * libxslt and passing pointers when creating stylesheets
                 */
                apr_hash_set(pathHash, thepath, APR_HASH_KEY_STRING, thepath);
            } else
                caLog(CA_LOG_WARN, "Unable to add path '%s' to XSLT pipe",
                      (char *)data);
        }
        xmlFree(data);
        if (rv != APR_SUCCESS)
            break;
    }

    if (rv != APR_SUCCESS)
        return rv;

    if (xd->templateFn) {
        if (strcmp((char *)xd->templateFn, "OUTPUT")) {
            rv = caGeneratorGetFilename(&xd->fsPath, (char *)xd->templateFn,
                                    pi->gen);
            if (rv == APR_SUCCESS) {
                xd->transform = xsltParseStylesheetFile((xmlChar *)xd->fsPath);
            }
        } else
            p->flags |= CAP_NOSWAP;
    }

    p->privateData = xd;    
    *pp = p;

    return APR_SUCCESS;
}

static apr_status_t caXsltInit(apr_pool_t *pool)
{
    apr_status_t rv;
    xsltSecurityPrefsPtr sec = NULL;

    rv = apr_pool_create(&xsltPool, pool);
    if (rv != APR_SUCCESS)
        return rv;
    pathHash = apr_hash_make(xsltPool);
    if (!pathHash)
        return APR_ENOMEM;

    xmlInitMemory();

    LIBXML_TEST_VERSION

    sec = xsltNewSecurityPrefs();
    xsltSetDefaultSecurityPrefs(sec);

    defaultEntityLoader = xmlGetExternalEntityLoader();
    xmlSetExternalEntityLoader(xsltExternalEntityLoader);

    xsltRegisterAllExtras();
    exsltRegisterAll();

    caPipeRegister("xslt", xsltPipePrepare);

    return APR_SUCCESS;
}

static apr_status_t caXsltFini(void)
{
    xmlCleanupParser();
    return APR_SUCCESS;
}

struct caModule ConArtistModule = {
    0,                           /* verMajor */
    1,                           /* verMinor */
    "XSLT Pipe Module",          /* name */
    CAM_PIPE,                    /* type */
    &caXsltInit,                 /* init */
    &caXsltFini,                 /* fini */
};
